La preuve par le code source : Bing Search et Bing Ads dans l'API Qwant de décembre 2016
Avertissement probatoire
Ce chapitre décrit les capacités du code versionné dans le dépôt
API-API-TAG-PACKAGER, au tagv1.32.1du 22 décembre 2016. Une capacité inscrite dans une version de release n'établit pas, à elle seule, son activation sur tout le trafic, le volume effectivement traité, la durée de conservation des données, ni l'exploitation qui en a été faite. Les extraits sont reproduits verbatim ; les clés d'interface et les adresses de messagerie y sont masquées, sans que cela modifie le fait constaté. Chaque pièce est accompagnée des éléments permettant à un tiers de la retrouver et de la vérifier par lui-même.
Objet : rassembler en un seul document vérifiable les preuves techniques de la dépendance de Qwant à Microsoft — moteur de recherche et régie publicitaire — dispersées jusqu'ici entre plusieurs chapitres, et publier le protocole permettant de les reproduire.
Date de la revue : 5 août 2026.
1. Réponse synthétique
Au 22 décembre 2016, le code de l'API Qwant établit neuf faits.
Ce que ce corpus établit :
- la totalité des recherches Web part chez Microsoft, la liste des langues réservées au moteur interne étant mise en commentaire dans le code ;
- le nombre de recherches autorisées sur l'interface
Bing V5est plafonné par le nombre d'appels publicitaires émis le même jour : recherche et publicité sont couplées dans le code lui-même ; - deux interfaces Microsoft distinctes coexistent (
bingapis.cometapi.datamarket.azure.com), la seconde servant de déversoir lorsque le quota de la première est atteint ; - les résultats sont conservés en cache sept jours ;
- chaque appel publicitaire transmet à Microsoft le User-Agent réel du visiteur, son adresse IP tronquée au dernier octet et son référent HTTP ;
- la fonction nommée
anonymizeIp()ne supprime que le dernier octet, ce qui désigne un réseau d'au plus 256 hôtes ; - la relation publicitaire repose sur des identifiants contractuels : deux propriétés Microsoft et douze unités publicitaires réparties sur six pays ;
- la clé d'accès à l'interface de recherche est inscrite en clair dans le dépôt, rattachée en commentaire à des comptes de messagerie nominatifs ;
- l'épuisement du crédit du compte Microsoft déclenche un courriel intitulé « [URGENT] un des compte n'a plus de crédit ! » adressé à trois destinataires personnels, dont celui du dirigeant.
Ce que ce corpus n'établit pas :
- le volume de trafic réellement servi par chacun des deux endpoints en production ;
- l'existence, le contenu ou les stipulations du contrat conclu avec Microsoft ;
- la durée de conservation effective, côté Microsoft, des données transmises ;
- une intention de tromper le public ou les financeurs, qui relève de l'appréciation judiciaire.
Ce chapitre est une pièce de vérification. Il ne rejoue pas les analyses déjà conduites ailleurs dans le dossier : il en fournit le socle reproductible et y renvoie systématiquement.
2. Provenance et protocole d'acquisition
Les constats portent sur un clone local du dépôt API-API-TAG-PACKAGER, dépôt de l'API front de Qwant (qwant_v2), une application PHP 5 bâtie sur CodeIgniter 2.
Remotes origin git@git.qwant.ninja:api/qwant_v2.git
github git@github.com:stephaneerard/qwantv2.git
Commits 13 070 accessibles depuis le tag v1.32.1
Étendue 30 avril 2012 → 22 décembre 2016
Tags 185, de v1.13.0 à v1.32.1
Volume 1 337 fichiers au tag v1.32.1, dont 1 281 fichiers .phpRemotes origin git@git.qwant.ninja:api/qwant_v2.git
github git@github.com:stephaneerard/qwantv2.git
Commits 13 070 accessibles depuis le tag v1.32.1
Étendue 30 avril 2012 → 22 décembre 2016
Tags 185, de v1.13.0 à v1.32.1
Volume 1 337 fichiers au tag v1.32.1, dont 1 281 fichiers .phpPoint de référence retenu pour l'ensemble du chapitre :
tag v1.32.1 (objet 1554284e8)
commit 22aa5650889b1a77e1ef9008ea55fef30cc8916f
date 2016-12-22 12:59:33 +0000
committer Sampson SAVINEL <s.s***@qwant.com>
sujet Merge branch 'prelive' into 'master'tag v1.32.1 (objet 1554284e8)
commit 22aa5650889b1a77e1ef9008ea55fef30cc8916f
date 2016-12-22 12:59:33 +0000
committer Sampson SAVINEL <s.s***@qwant.com>
sujet Merge branch 'prelive' into 'master'Ce tag est le même que celui retenu aux chapitres 22, 40 et 55. Les blobs de search_web.php et de search_ads.php relevés ici sont identiques à ceux publiés au chapitre 40, section 8.2 : les deux relevés, effectués à des dates différentes, concordent.
Précaution méthodologique
La branche
developdu clone s'arrête au 6 juillet 2016 (a4d5a9a1d00afbb9e6ceff4cbc89be4f92991841), soit avant le tagv1.32.1. Les deux points de référence ne sont pas interchangeables : le modèlebing_error.php, présent surdevelop, a été supprimé avant décembre, alors que la fonction d'alerte qu'il appelait subsiste dansMailer.php(pièce n° 8). Aucun constat de ce chapitre ne mélange les deux référentiels.
Trois commandes suffisent à reproduire chaque ligne de l'annexe :
git -C <dépôt> rev-parse v1.32.1:<chemin>
git -C <dépôt> cat-file blob <blob> | sha256sum
git -C <dépôt> log -1 --format='%h|%cs|%cn' v1.32.1 -- <chemin>git -C <dépôt> rev-parse v1.32.1:<chemin>
git -C <dépôt> cat-file blob <blob> | sha256sum
git -C <dépôt> log -1 --format='%h|%cs|%cn' v1.32.1 -- <chemin>La première donne l'identifiant de l'objet Git, la deuxième l'empreinte SHA-256 de son contenu exact, la troisième le dernier commit ayant modifié le fichier à cette version et le nom de son auteur. Sous PowerShell, la deuxième commande s'écrit git cat-file blob <blob> | Get-FileHash -Algorithm SHA256.
3. Chronologie de la dépendance
| Date | Commit | Committer | Fait |
|---|---|---|---|
| 30 avril 2012 | bd61943c6 |
JCC | Import initial du dépôt. Il contient déjà des appels à api.bing.net. |
| 31 juillet 2012 | cdc4545aa |
Jcchemin | Migration de l'API Bing 2.0 vers Azure DataMarket. |
| 5 juin 2013 | 39c769e9c |
Jcchemin | Création du modèle d'alerte « compte Bing vide ». |
| 16 mars 2016 | c10af1941 |
p.vignaux | #API-394 bing instead of yboss : Bing passe de solution de repli à fournisseur principal. |
| 30 mars 2016 | ac6a6f049 |
p.vignaux | Durée de cache des résultats Web et Images portée à sept jours. |
| 15 juin 2016 | c094aa279 |
Jonathan (j.cassar) | add real user_agent and anonymized client ip for bing ads call. |
| 17 juin 2016 | b4ba79ddf |
p.vignaux | Création de search_ads.php. |
| 22 décembre 2016 | 22aa5650 |
Sampson SAVINEL | Tag v1.32.1, point de référence du présent chapitre. |
Deux enseignements. D'abord, Bing est présent dès le premier jour du dépôt : la dépendance n'est pas une dérive tardive, c'est le point de départ. Ensuite, la bascule décisive n'est pas technique mais contractuelle : en mars 2016, Bing cesse d'être un repli pour devenir la source. L'analyse détaillée de cette bascule figure au chapitre 22.
4. Pièce n° 1 — Le routage : la liste des langues réservées au moteur interne est mise en commentaire
Dans www/application/models/search/search_web.php, la propriété qui détermine quelles langues sont servies par le moteur interne « WebBrain » est déclarée ainsi, à la ligne 11 :
/**
* @var array
* Authorized locales for WebBrain calls
*/
private $webBrainLocales = [/*'fr', 'de'*/]; /**
* @var array
* Authorized locales for WebBrain calls
*/
private $webBrainLocales = [/*'fr', 'de'*/];Les deux valeurs 'fr' et 'de' sont présentes mais neutralisées par un commentaire. Le tableau est donc vide à l'exécution. Or c'est lui qui commande l'aiguillage, lignes 51 à 55 :
public function search($keyword, $locale, $count = 50, $offset = 0, $filters = false)
{
if (in_array($locale[0], $this->webBrainLocales)) {
$type = 'webBrain';
$version = 'W';
$searchUrl = $this->getWebBrainUrl($keyword, $locale, $offset, $count, $filters);
} else { public function search($keyword, $locale, $count = 50, $offset = 0, $filters = false)
{
if (in_array($locale[0], $this->webBrainLocales)) {
$type = 'webBrain';
$version = 'W';
$searchUrl = $this->getWebBrainUrl($keyword, $locale, $offset, $count, $filters);
} else {Ce code fait ceci : il teste si la langue de la requête figure dans une liste vide. Ce test ne peut jamais réussir. Toute recherche Web, quelle que soit sa langue, emprunte donc la branche else, c'est-à-dire Microsoft. Le moteur interne n'est pas absent du code — il est présent, appelable, et désactivé d'une ligne de commentaire.
L'historique de cette propriété et la question de son déploiement sont traités aux chapitres 22 et 40, ainsi que dans l'analyse forensique du dépôt.
5. Pièce n° 2 — Le quota : le nombre de recherches est plafonné par le nombre d'annonces affichées
C'est la pièce la plus significative de ce chapitre, et elle n'avait pas encore été relevée. La branche else de la fonction search(), lignes 55 à 70 :
} else {
// Enable Bing V5 calls until their number is equal to Ads queries count
$adsQueryCount = intval($this->getCache(Search_ads::$bingV5CallsCountCacheKey));
$queryCount = intval($this->getCache(static::$bingV5CallsCountCacheKey));
if ($queryCount <= $adsQueryCount || config_item('bing_apiv5_search_web_enabled') === true) {
$type = 'bingV5';
$version = 'B' . ($adsQueryCount - $queryCount);
$searchUrl = $this->getApiUrlBingV5($keyword, $locale, $offset, $count, $filters);
$this->setCache(static::$bingV5CallsCountCacheKey, $queryCount + 1, static::$bingV5CallsCountCacheDuration);
} else {
$type = 'bingAzure';
$version = 'A' . ($adsQueryCount - $queryCount);
$this->getContainer()->getHttpService()->setBingKey(BING_KEY_WEBONLY);
$searchUrl = $this->getApiUrlBingAzure($keyword, $locale, $offset, $filters);
}
} } else {
// Enable Bing V5 calls until their number is equal to Ads queries count
$adsQueryCount = intval($this->getCache(Search_ads::$bingV5CallsCountCacheKey));
$queryCount = intval($this->getCache(static::$bingV5CallsCountCacheKey));
if ($queryCount <= $adsQueryCount || config_item('bing_apiv5_search_web_enabled') === true) {
$type = 'bingV5';
$version = 'B' . ($adsQueryCount - $queryCount);
$searchUrl = $this->getApiUrlBingV5($keyword, $locale, $offset, $count, $filters);
$this->setCache(static::$bingV5CallsCountCacheKey, $queryCount + 1, static::$bingV5CallsCountCacheDuration);
} else {
$type = 'bingAzure';
$version = 'A' . ($adsQueryCount - $queryCount);
$this->getContainer()->getHttpService()->setBingKey(BING_KEY_WEBONLY);
$searchUrl = $this->getApiUrlBingAzure($keyword, $locale, $offset, $filters);
}
}Les deux compteurs sont des clés de cache journalières, définies dans les constructeurs respectifs des deux modèles :
// www/application/models/search/search_web.php, ligne 28
static::$bingV5CallsCountCacheKey = 'bing_v5_web_calls_count_' . date('Ymd');
// www/application/models/search/search_ads.php, ligne 57
static::$bingV5CallsCountCacheKey = 'bing_v5_ads_calls_count_' . date('Ymd');// www/application/models/search/search_web.php, ligne 28
static::$bingV5CallsCountCacheKey = 'bing_v5_web_calls_count_' . date('Ymd');
// www/application/models/search/search_ads.php, ligne 57
static::$bingV5CallsCountCacheKey = 'bing_v5_ads_calls_count_' . date('Ymd');Et le compteur publicitaire est incrémenté à chaque appel de la régie, dans search_ads.php, lignes 145 à 147 :
// Count impressions
$queryCount = intval($this->getCache(static::$bingV5CallsCountCacheKey));
$this->setCache(static::$bingV5CallsCountCacheKey, $queryCount + 1, static::$bingV5CallsCountCacheDuration); // Count impressions
$queryCount = intval($this->getCache(static::$bingV5CallsCountCacheKey));
$this->setCache(static::$bingV5CallsCountCacheKey, $queryCount + 1, static::$bingV5CallsCountCacheDuration);Ce code fait ceci : chaque jour, Qwant compte séparément le nombre d'appels envoyés à la régie publicitaire Microsoft et le nombre de recherches envoyées à l'interface de recherche Bing V5. Tant que le second reste inférieur ou égal au premier, la recherche passe par Bing V5. Dès que le second dépasse le premier, le trafic bascule sur l'interface Azure DataMarket, celle qui consomme le crédit du compte payant.
Ce qui l'implique : le droit d'interroger l'interface de recherche est gagé sur le fait d'avoir affiché des publicités, dans un rapport de un pour un, appliqué automatiquement, jour par jour. Le commentaire du développeur l'énonce sans ambiguïté — « Enable Bing V5 calls until their number is equal to Ads queries count ». La variable $version, qui transporte l'écart entre les deux compteurs sous la forme B-42 ou A17, montre que cet écart était suivi.
Ce constat éclaire d'un jour concret la question du coût traitée au chapitre 21 : l'accès à la recherche n'est pas seulement acheté, il est échangé contre de l'inventaire publicitaire, et l'échange est arbitré par le code en temps réel.
6. Pièce n° 3 — Deux interfaces Microsoft, et un cache de sept jours
Les deux fonctions de construction d'URL coexistent dans le même fichier. Ligne 126, l'interface Bing V5 :
public function getApiUrlBingV5($keyword, $locale, $offset, $count, $filters)
{
$apiCall = 'https://www.bingapis.com/api/v5/search?appid=' . config_item('bing_app_id') . '&responseFilter=Webpages&format=json&offset=' . $offset . '&count=' . $count . '&mkt=' . implode('-', $locale) . '&q=' . urlencode($keyword); public function getApiUrlBingV5($keyword, $locale, $offset, $count, $filters)
{
$apiCall = 'https://www.bingapis.com/api/v5/search?appid=' . config_item('bing_app_id') . '&responseFilter=Webpages&format=json&offset=' . $offset . '&count=' . $count . '&mkt=' . implode('-', $locale) . '&q=' . urlencode($keyword);Ligne 155, l'interface Azure DataMarket :
public function getApiUrlBingAzure($keyword, $locale, $offset, $filters)
{
$apiCall = 'https://api.datamarket.azure.com/Bing/SearchWeb/v1/Web?$format=json&$skip=' . $offset . '&Market=%27' . implode('-', $locale) . '%27&Query=%27' . urlencode($keyword) . '%27'; public function getApiUrlBingAzure($keyword, $locale, $offset, $filters)
{
$apiCall = 'https://api.datamarket.azure.com/Bing/SearchWeb/v1/Web?$format=json&$skip=' . $offset . '&Market=%27' . implode('-', $locale) . '%27&Query=%27' . urlencode($keyword) . '%27';Ce code fait ceci : il transmet à Microsoft, en clair dans l'URL, le texte intégral de la requête de l'utilisateur (q ou Query), son marché linguistique (mkt ou Market), sa position de pagination et son réglage de filtrage familial. Ce sont les paramètres de la recherche elle-même qui quittent l'infrastructure Qwant.
La dépendance n'est pas cantonnée au Web. Les autres verticales suivent le même schéma :
| Verticale | Fichier et ligne | Interface appelée |
|---|---|---|
| Web | search_web.php:126 |
https://www.bingapis.com/api/v5/search |
| Web | search_web.php:155 |
https://api.datamarket.azure.com/Bing/SearchWeb/v1/Web |
| Actualités | search_news.php:222 |
https://www.bingapis.com/api/v5/news/search |
| Actualités | search_news.php:258 |
https://api.datamarket.azure.com/Bing/Search/v1/News |
| Images | search_images.php:233 |
https://www.bingapis.com/api/v5/images/search |
| Images | search_images.php:287 |
https://api.datamarket.azure.com/Bing/Search/v1/Image |
| Publicité | search_ads.php:232 |
https://www.bingapis.com/api/v5/ads/search |
Pour les actualités, le repli vers Microsoft est commandé par une liste de langues « couvertes par l'univers » interne, search_news.php ligne 13 :
protected $universeLanguages = ['fr', 'de', 'en', 'it', 'co']; protected $universeLanguages = ['fr', 'de', 'en', 'it', 'co'];Toute langue absente de cette liste est servie par Bing News. On relèvera au passage que l'appel News V5 est authentifié avec l'identifiant applicatif de la régie publicitaire et non celui de la recherche (search_news.php:222 : config_item('bing_ads_app_id_search')), ce qui confirme que les deux relations passent par le même compte Microsoft.
Enfin, la durée de conservation des résultats est fixée ligne 35 :
/**
* @return int
* Web results cache lifetime
*/
public function getCacheLifetime()
{
return 3600 * 24 * 7;
} /**
* @return int
* Web results cache lifetime
*/
public function getCacheLifetime()
{
return 3600 * 24 * 7;
}Soit sept jours. La discussion de la compatibilité de cette durée avec les conditions d'utilisation publiques de l'API Bing figure au chapitre 22 ; elle n'est pas reprise ici.
7. Pièce n° 4 — L'authentification : Microsoft est câblé dans la passerelle HTTP
Le fichier www/application/models/httpgateway.php centralise tous les appels sortants. Lignes 181 à 183 :
private function setupAuthentication($url, $curlHandler)
{
if (preg_match('/api.datamarket.azure.com/', $url) > 0) {
curl_setopt($curlHandler, CURLOPT_HTTPAUTH, CURLAUTH_BASIC);
curl_setopt($curlHandler, CURLOPT_USERPWD, "ignored:" . $this->bingKey); private function setupAuthentication($url, $curlHandler)
{
if (preg_match('/api.datamarket.azure.com/', $url) > 0) {
curl_setopt($curlHandler, CURLOPT_HTTPAUTH, CURLAUTH_BASIC);
curl_setopt($curlHandler, CURLOPT_USERPWD, "ignored:" . $this->bingKey);Ce code fait ceci : il reconnaît l'hôte Microsoft par expression régulière et lui présente la clé Qwant en authentification HTTP Basic, le nom d'utilisateur étant littéralement ignored. Ce qui l'implique : la relation avec Microsoft n'est pas un branchement périphérique ajouté dans un module de recherche, elle est inscrite dans la couche de transport commune de l'application.
La même passerelle tient un relevé des appels sortants, lignes 229 à 231 :
$wantedHosts = [
'api.datamarket.azure.com' => 'bing',
'www.bingapis.com' => 'bing', $wantedHosts = [
'api.datamarket.azure.com' => 'bing',
'www.bingapis.com' => 'bing',Les deux hôtes Microsoft ouvrent la liste et sont rattachés à un même domaine logique, bing. Le devenir de ces relevés est traité au chapitre 55.
Un détail mérite d'être noté par contraste avec la pièce n° 6 : pour les appels de recherche, la passerelle substitue un User-Agent fixe à celui du visiteur, ligne 17 :
$this->defaultUserAgent = "Mozilla/5.0 (Windows; U; Windows NT 5.1; " . "en-US; rv:1.8.1.1) Gecko/20061204 Firefox/2.0.0.1"; $this->defaultUserAgent = "Mozilla/5.0 (Windows; U; Windows NT 5.1; " . "en-US; rv:1.8.1.1) Gecko/20061204 Firefox/2.0.0.1";L'équipe savait donc masquer le navigateur de l'utilisateur, et le faisait pour la recherche. La pièce n° 6 montre qu'elle ne le faisait pas pour la publicité.
8. Pièce n° 5 — La clé d'accès est inscrite en clair dans le dépôt
Dans www/application/config/constants_common.php, lignes 38 et 39, reproduites ici avec la valeur des clés et les adresses masquées :
//define('BING_KEY_WEBONLY', '***REDACTED***'); // q.q***@gmail.com
define('BING_KEY_WEBONLY', '***REDACTED***'); // m.j***@qwant.com//define('BING_KEY_WEBONLY', '***REDACTED***'); // q.q***@gmail.com
define('BING_KEY_WEBONLY', '***REDACTED***'); // m.j***@qwant.comCe code fait ceci : il définit, en clair dans un fichier versionné, la clé unique utilisée par toutes les recherches Azure DataMarket — Web, Actualités et Images. La ligne précédente conserve la clé antérieure, également en clair, en commentaire.
Deux constats en découlent. Sur le plan de la sécurité, un secret d'interface engageant un compte facturé a été committé dans un dépôt et y demeure accessible dans l'historique. Sur le plan de la caractérisation de la relation, les commentaires rattachent ces clés à des comptes de messagerie nominatifs, dont l'un est une adresse @gmail.com : l'accès à l'infrastructure de recherche de Microsoft était administré sur des comptes personnels.
Les valeurs réelles ne sont pas reproduites ici ; elles sont vérifiables par un tiers dans le blob 12c2c3c582ce8e8db67ac186a71a0308d70f84f0, lignes 38 et 39.
9. Pièce n° 6 — Ce que chaque appel publicitaire transmet à Microsoft
Dans www/application/models/search/search_ads.php, lignes 133 à 138, le commentaire du développeur précède l'énumération :
// Bing Ads required headers:
// Referer, IP address, User agent
$referer = !empty($_SERVER['HTTP_REFERER']) ? $_SERVER['HTTP_REFERER'] : config_item('front_url_' . config_item(API_APP));
$ipAddress = $this->input->ip_address();
$ipAddressAnonymized = $this->anonymizeIp($ipAddress);
$userAgent = $this->input->user_agent(); // Bing Ads required headers:
// Referer, IP address, User agent
$referer = !empty($_SERVER['HTTP_REFERER']) ? $_SERVER['HTTP_REFERER'] : config_item('front_url_' . config_item(API_APP));
$ipAddress = $this->input->ip_address();
$ipAddressAnonymized = $this->anonymizeIp($ipAddress);
$userAgent = $this->input->user_agent();Puis lignes 154 à 157, ces valeurs sont posées sur la requête sortante :
$extraOptions = [];
$extraOptions[CURLOPT_USERAGENT] = $userAgent;
$extraOptions[CURLOPT_HTTPHEADER] = ['X-Search-ClientIP: ' . $ipAddressAnonymized];
$extraOptions[CURLOPT_REFERER] = $referer; $extraOptions = [];
$extraOptions[CURLOPT_USERAGENT] = $userAgent;
$extraOptions[CURLOPT_HTTPHEADER] = ['X-Search-ClientIP: ' . $ipAddressAnonymized];
$extraOptions[CURLOPT_REFERER] = $referer;Ce code fait ceci : il transmet à Microsoft, pour chaque affichage publicitaire, le User-Agent réel du navigateur du visiteur, son adresse IP amputée du dernier octet, et l'URL de provenance. S'y ajoutent, dans l'URL de l'appel, le texte de la requête et le marché.
Le mot « anonymisée » employé dans le nom de la variable renvoie à cette fonction, lignes 422 à 427 :
/**
* @param $ip
*
* @return string
* Replace last digits with 0 from an IP address
*/
private function anonymizeIp($ip)
{
$lastPoint = strrpos($ip, '.');
return ($lastPoint !== false) ? substr($ip, 0, $lastPoint) . '.0' : $ip;
} /**
* @param $ip
*
* @return string
* Replace last digits with 0 from an IP address
*/
private function anonymizeIp($ip)
{
$lastPoint = strrpos($ip, '.');
return ($lastPoint !== false) ? substr($ip, 0, $lastPoint) . '.0' : $ip;
}Ce que cela implique : la transformation remplace le dernier octet par zéro. Elle conserve donc le préfixe /24, qui désigne un réseau d'au plus 256 adresses. Combinée au User-Agent complet et au référent, elle ne fait pas obstacle à la ré-identification. Cette question est traitée au fond aux chapitres 33 et 53 ; l'analyse des en-têtes figure aux chapitres 9 et 40.
On relèvera enfin qu'un dispositif de répartition A/B, mis en commentaire lignes 220 à 230, calculait un identifiant de visiteur par hash('sha256', $config['client_ip'] . SALT_CLIENT_HASH) à partir de l'adresse IP non tronquée, transportée jusque-là dans $config['client_ip'] (ligne 152). Le dispositif est désactivé au tag examiné ; sa présence montre que l'IP complète restait disponible côté application.
10. Pièce n° 7 — Les identifiants contractuels : deux propriétés, douze unités publicitaires
L'URL de l'appel publicitaire, lignes 232 à 239 :
$apiCall = 'https://www.bingapis.com/api/v5/ads/search?appid=' . config_item('bing_ads_app_id_' . config_item(API_APP)) .
'&adUnitId=' . $adUnitId .
'&propertyId=' . config_item('bing_ads_property_id_' . config_item(API_APP)) .
'&q=' . urlencode($keywords) .
'&mainlineCount=' . $count .
'&pageNumber=' . $page .
'&mkt=' . urlencode($mkt) .
'&textDecorations=true&textFormat=html'; $apiCall = 'https://www.bingapis.com/api/v5/ads/search?appid=' . config_item('bing_ads_app_id_' . config_item(API_APP)) .
'&adUnitId=' . $adUnitId .
'&propertyId=' . config_item('bing_ads_property_id_' . config_item(API_APP)) .
'&q=' . urlencode($keywords) .
'&mainlineCount=' . $count .
'&pageNumber=' . $page .
'&mkt=' . urlencode($mkt) .
'&textDecorations=true&textFormat=html';Les identifiants correspondants, dans www/application/config/config_common.php, lignes 96 à 116 :
$config['bing_ads_app_id_search'] = '***REDACTED***';
$config['bing_ads_app_id_lite'] = '***REDACTED***';
$config['bing_ads_extensions'] = 'LongAdTitle,EnhancedSiteLinks';
$config['bing_ads_items_max'] = 2;
$config['bing_ads_property_id_search'] = 345761;
$config['bing_ads_property_id_lite'] = 349323;
$config['bing_ads_unit_search'] = [
'fr' => 300774,
'de' => 300775,
'gb' => 300776,
'ca' => 300777,
'us' => 300778,
'ch' => 300779
];
$config['bing_ads_unit_lite'] = [
'fr' => 311248,
'de' => 311247,
'gb' => 311249,
'ca' => 311245,
'us' => 311250,
'ch' => 311246
];$config['bing_ads_app_id_search'] = '***REDACTED***';
$config['bing_ads_app_id_lite'] = '***REDACTED***';
$config['bing_ads_extensions'] = 'LongAdTitle,EnhancedSiteLinks';
$config['bing_ads_items_max'] = 2;
$config['bing_ads_property_id_search'] = 345761;
$config['bing_ads_property_id_lite'] = 349323;
$config['bing_ads_unit_search'] = [
'fr' => 300774,
'de' => 300775,
'gb' => 300776,
'ca' => 300777,
'us' => 300778,
'ch' => 300779
];
$config['bing_ads_unit_lite'] = [
'fr' => 311248,
'de' => 311247,
'gb' => 311249,
'ca' => 311245,
'us' => 311250,
'ch' => 311246
];Ce code fait ceci : il déclare deux propriétés publicitaires Microsoft distinctes — 345761 pour le produit principal, 349323 pour le produit « lite » — et douze unités publicitaires réparties sur six pays pour chacune d'elles. Les identifiants applicatifs sont masqués ici car ils s'apparentent à des secrets ; les numéros de propriété et d'unité, eux, sont reproduits en clair car ce ne sont pas des secrets et ce sont eux qui portent la valeur probante.
Ce que cela implique : de tels identifiants ne s'obtiennent pas par une inscription libre à une interface publique. Ils sont attribués par la régie au terme d'un référencement, produit par produit et pays par pays. Leur présence dans le code établit l'existence d'une relation de monétisation formalisée entre Qwant et Microsoft Advertising, portant sur deux produits et six marchés.
Deux réserves accompagnent ce constat. D'une part, les interrupteurs bing_ads_enabled_search et bing_ads_enabled_lite sont à false dans la configuration commune du tag examiné, la valeur effective dépendant des surcharges d'environnement. D'autre part, bing_ads_testing_search et bing_ads_testing_lite valent true, ce qui ajoute &trafficType=test&form=monitr à l'appel. Le code établit donc l'intégration et son périmètre contractuel, non le volume commercial effectivement servi à cette date.
11. Pièce n° 8 — L'épuisement du crédit Microsoft alerte la direction
Dans www/application/libraries/Mailer.php, lignes 409 à 419, adresses masquées :
public function sendAlertBing()
{
$list_user = array('e.l***@gmail.com', 'e.b***@gmail.com', 'j.c***@gmail.com');
$title = '[URGENT] un des compte n\'a plus de crédit !';
$message = 'Un des comptes n\'a plus de crédit, il faut impérativement le recharger immédatement.';
foreach ($list_user as $user) {
$this->doSendMail($title, $message, $user);
}
} public function sendAlertBing()
{
$list_user = array('e.l***@gmail.com', 'e.b***@gmail.com', 'j.c***@gmail.com');
$title = '[URGENT] un des compte n\'a plus de crédit !';
$message = 'Un des comptes n\'a plus de crédit, il faut impérativement le recharger immédatement.';
foreach ($list_user as $user) {
$this->doSendMail($title, $message, $user);
}
}Ce code fait ceci : lorsqu'un compte Microsoft arrive à court de crédit, il expédie un courriel qualifié d'urgent à trois destinataires, en leur demandant de recharger « immédatement » — la faute d'orthographe est dans le code source.
Ce que cela implique tient en une phrase : si le crédit Microsoft s'épuise, le moteur de recherche s'arrête. On ne construit pas une procédure d'escalade de ce type pour un fournisseur accessoire ; on la construit pour une dépendance dont la rupture interrompt le service. Le fait que les trois destinataires soient des adresses personnelles, dont celle du dirigeant, indique le niveau auquel l'incident était traité.
Cette fonction est le vestige d'un dispositif plus ancien : le modèle bing_error.php, créé le 5 juin 2013, qui journalisait les erreurs Bing en base et déclenchait cette alerte au plus une fois toutes les cinq minutes. Ce modèle a été supprimé avant décembre 2016, mais la fonction d'alerte subsiste dans le tag examiné, désormais sans appelant. Sa présence documente une période d'au moins trois ans et demi, de juin 2013 à décembre 2016, pendant laquelle l'indisponibilité de Bing était traitée comme un incident critique.
12. Ce que le code prouve
- Au 22 décembre 2016, aucune recherche Web ne peut être servie par le moteur interne : la liste des langues qui l'autorisent est vide, ses deux valeurs étant mises en commentaire.
- Le nombre de recherches adressées à l'interface
Bing V5est plafonné chaque jour par le nombre d'appels publicitaires émis, selon un rapport de un pour un inscrit dans le code et documenté par un commentaire de développeur. - Au-delà de ce plafond, le trafic bascule sur l'interface Azure DataMarket, qui consomme le crédit d'un compte payant.
- Le texte intégral des requêtes des utilisateurs, leur marché linguistique et leur réglage de filtrage familial sont transmis à Microsoft pour les verticales Web, Actualités, Images et pour la publicité.
- Les résultats sont conservés en cache sept jours.
- Chaque appel publicitaire transmet en outre le User-Agent réel du visiteur, son adresse IP tronquée au dernier octet et son référent HTTP.
- La fonction nommée
anonymizeIp()conserve le préfixe/24, qui désigne un réseau d'au plus 256 hôtes. - La relation publicitaire repose sur deux propriétés Microsoft et douze unités publicitaires réparties sur six pays, identifiants qui supposent un référencement préalable auprès de la régie.
- La clé d'accès à l'interface de recherche est inscrite en clair dans le dépôt et rattachée en commentaire à des comptes de messagerie nominatifs, dont un compte
@gmail.com. - Une procédure d'alerte adressait aux dirigeants, en cas d'épuisement du crédit Microsoft, un courriel qualifié d'urgent.
- La dépendance est présente dès le premier import du dépôt, le 30 avril 2012.
13. Ce qui reste à démontrer
- La part de trafic réellement servie par chacune des deux interfaces Microsoft en production, à cette date et sur la durée : les compteurs journaliers existent dans le code, leurs valeurs ne figurent pas dans le dépôt.
- Le contenu du contrat conclu avec Microsoft, et notamment si le rapport de un pour un entre recherches et annonces y est stipulé ou s'il procède d'un choix d'implémentation de Qwant.
- L'état effectif des interrupteurs
bing_ads_enabled_*sur les serveurs de production au 22 décembre 2016, les surcharges d'environnement n'étant pas toutes présentes dans le dépôt. - Le sort réservé par Microsoft aux données reçues : durée de conservation, croisement éventuel avec d'autres jeux, réutilisation.
- Le nombre de personnes concernées et la période exacte pendant laquelle les en-têtes publicitaires ont été transmis en production.
- Si les clés inscrites en clair ont fait l'objet d'une rotation après leur exposition, et si cette exposition a été signalée.
Annexe — Empreintes et objets Git
Empreintes relevées le 5 août 2026 sur le tag v1.32.1. La colonne « SHA-256 » porte sur le contenu exact du blob, obtenu par git cat-file blob <blob> | sha256sum. La colonne « Dernier commit » indique le dernier commit ayant modifié ce fichier à cette version, et le nom de son auteur.
| Fichier | Blob Git | Dernier commit | Date | Committer |
|---|---|---|---|---|
www/application/models/search/search_web.php |
d36ca4c6440552df106a722c73e86822c86bc9a1 |
22b32fdc0 |
21 novembre 2016 | p.vignaux |
www/application/models/search/search_ads.php |
6081e444d17b26cfbc60cfc2f444c8910f842818 |
5a422fff1 |
1er décembre 2016 | p.vignaux |
www/application/models/search/search_news.php |
bbf7b20dd79be6a2d75d1492f9697fa964744095 |
933189f67 |
17 novembre 2016 | Pierre |
www/application/models/search/search_images.php |
b8b5241d33d481b5e9421fb85239af1f2061bfec |
57429a973 |
18 novembre 2016 | p.vignaux |
www/application/models/httpgateway.php |
2b475c883d8b9e317eaf934e15dd603843af7f4b |
ebcb62f62 |
2 août 2016 | p.vignaux |
www/application/config/constants_common.php |
12c2c3c582ce8e8db67ac186a71a0308d70f84f0 |
78ad41fbb |
17 août 2016 | Sampson SAVINEL |
www/application/config/config_common.php |
00295cc10ec307449321538c4a4e42cd29ded49a |
ad767f7b5 |
16 décembre 2016 | p.vignaux |
www/application/libraries/Mailer.php |
d0b37b8191585822400a678f541e4329deb94bb7 |
ddd315c3a |
21 novembre 2016 | p.vignaux |
Empreintes SHA-256 des contenus correspondants :
| Fichier | SHA-256 |
|---|---|
search_web.php |
C3C44BFC781B4577D1963572AC6A260E712A51E8A33E7855D9C363DD21CA3DF7 |
search_ads.php |
05A38FE7D6284099491AB1BF9894E6565D554A788B01996ABD7B11A46CF43735 |
search_news.php |
078A4EE68B93E55F99F4BC363970459141C1E6C24D4DDAA0CD1C231FD18F277C |
search_images.php |
9C3B18BA119D57BBFF470C454C9A8EC3235F8208B2AB3EBE6E5DC1DF0B70362D |
httpgateway.php |
DFA8B45C88978746DA344A332CA27CA37C5D9758BF48BCC26CED04F96A182DAB |
constants_common.php |
E6A9C892D01E64DCD28D8C2C5EF210ED20A99F228A4FE2D702694520CA80381A |
config_common.php |
4B8A223551370240D9462BBF1AB7499A3AAE524D642AF60CDF5EC40203D4AA5C |
Mailer.php |
164D0BD6D7CDCDFBA673923D6A627FB0FD5825EC044C3DB728593D92D224EF6E |
Les blobs de search_web.php et search_ads.php sont identiques à ceux publiés à la section 8.2 du chapitre 40, relevés indépendamment à une date antérieure.
Pour une procédure contentieuse, une acquisition forensique complète devrait en outre documenter la provenance du clone, la liste de ses dépôts distants, le graphe complet des références, l'horodatage de l'acquisition et une empreinte de l'archive entière.
14. Articulation avec le reste du dossier
| Chapitre | Ce qu'il apporte |
|---|---|
| 9 — De l'alerte publique à l'alerte interne | Les alertes de 2016 sur les en-têtes transmis à Microsoft |
| 18 — Qwant ou Xilopix : exister d'abord, ou construire d'abord | La portée stratégique du choix de la dépendance |
| 19 — Qwantify en 2014 et 2015 | Ce qu'un crawler établit et n'établit pas |
| 21 — Ce que l'accès à Bing et Bing Ads a pu coûter | La reconstitution économique, que la pièce n° 2 vient éclairer |
| 22 — Faire « mouliner » la technologie de Qwant sur les résultats de Bing | L'historique Git de search_web.php et la question contractuelle du cache |
| 33 — État de l'art de la ré-identification | Pourquoi un préfixe /24 et un User-Agent ne sont pas anonymes |
| 40 — Fraude aux financements publics | La confrontation du code aux déclarations faites aux financeurs |
| 49 — Qwant en 2020 : index figé, liens morts | La suite de l'histoire, quatre ans plus tard |
| 53 — L'avertissement prouve que Qwant savait | La connaissance interne de l'enjeu du préfixe IPv4 |
| 55 — DataHub | Ce que devenaient, côté Qwant, les événements liés à ces appels |
| Analyse forensique du dépôt Git | La méthode d'examen du dépôt et la branche demo |
Pièces locales
- dépôt local
API-API-TAG-PACKAGER, tagv1.32.1, commit22aa5650889b1a77e1ef9008ea55fef30cc8916fdu 22 décembre 2016 ; - blobs Git et empreintes SHA-256 listés à l'annexe ci-dessus, reproductibles par les trois commandes de la section 2 ;
- commits cités à la section 3 :
bd61943c,cdc4545a,39c769e9,c10af194,ac6a6f04,c094aa27,b4ba79dd; - Contributeurs du dépôt Git — relevé par auteur issu de
git log --all; - Faire « mouliner » la technologie de Qwant sur les résultats de Bing ;
- Fraude aux financements publics : la preuve par les documents ;
- DataHub : le tracking serveur opéré par Qwant ;
- Analyse forensique détaillée du dépôt git API-API-TAG-PACKAGER.
Sources externes
- Documentation historique de l'API Bing Search sur Azure DataMarket (ouvre dans une nouvelle fenêtre) (https://web.archive.org/web/20160304053957/https://datamarket.azure.com/dataset/bing/search (ouvre dans une nouvelle fenêtre)) ;
- Conditions d'utilisation de la Bing Search API du 12 juin 2015 (ouvre dans une nouvelle fenêtre) (https://www.carahsoft.com/buy/gsa-schedule-contracts/approved-csas/docFileDownload/286450/7184/21444653/0#page=15 (ouvre dans une nouvelle fenêtre)) ;
- Avis WP216 du Groupe de travail « Article 29 » sur les techniques d'anonymisation, 10 avril 2014 (ouvre dans une nouvelle fenêtre) (https://ec.europa.eu/justice/article-29/documentation/opinion-recommendation/files/2014/wp216_fr.pdf (ouvre dans une nouvelle fenêtre)).
Conclusion probatoire
Le code du tag
v1.32.1ne se borne pas à montrer que Qwant appelait Bing : il montre que la recherche et la publicité étaient couplées par construction. Le droit d'interroger l'interface de recherche y est gagé, jour après jour et dans un rapport de un pour un, sur le nombre d'annonces affichées ; au-delà, le trafic bascule sur un compte facturé dont l'épuisement déclenche une alerte urgente vers la direction. La formulation la plus étayée est donc celle-ci : à cette date, l'existence même du service de recherche dépendait, dans le code, du maintien de la relation commerciale avec Microsoft.Ce constat porte sur une version de release. Il n'établit ni les volumes servis, ni les stipulations du contrat, ni l'intention de ceux qui l'ont écrit. Il établit une architecture, et cette architecture est vérifiable par quiconque dispose du dépôt et des empreintes publiées à l'annexe.